import { MarkerWithLabel } from '@googlemaps/markerwithlabel';
import { assign, find, head, isEmpty, isNil, isNumber, isString, last } from 'lodash-es';
import { useEventBus } from '../event-bus';
import {
  DEFAULTS_CENTER_LOCATION,
  LocationTag,
  PolylineStrokeColor,
  type ILocation,
} from './constants';
import { build_marker } from './marker';
import type { IMarkerBuilder, IPlace, IPlaceList, IProps } from './types';

type IGetAddress = (i: ILocation) => string;

const event_bus = useEventBus();
const DEFAULT_ZOOM = 13;
const EVENT_MARKER_CLICK = '/google-map/marker/click';

function _get_google_map_config(args: Partial<IProps> = {}): google.maps.MapOptions {
  const { disableDefaultUI = true, zoom = DEFAULT_ZOOM } = args;
  const conf: google.maps.MapOptions = {
    center: DEFAULTS_CENTER_LOCATION,
    disableDefaultUI,
    fullscreenControl: true,
    fullscreenControlOptions: {
      position: google.maps.ControlPosition.TOP_RIGHT,
    },
    mapTypeControl: false,
    mapTypeControlOptions: {
      mapTypeIds: ['roadmap'],
      style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
    },
    streetViewControl: false,
    styles: [
      {
        featureType: 'all',
        elementType: 'geometry.fill',
        stylers: [
          {
            weight: 2.0,
          },
        ],
      },
      {
        featureType: 'all',
        elementType: 'geometry.stroke',
        stylers: [
          {
            color: '#9c9c9c',
          },
        ],
      },
      {
        featureType: 'all',
        elementType: 'labels.text',
        stylers: [
          {
            visibility: 'on',
          },
        ],
      },
      {
        featureType: 'landscape',
        elementType: 'all',
        stylers: [
          {
            color: '#f2f2f2',
          },
        ],
      },
      {
        featureType: 'landscape',
        elementType: 'geometry.fill',
        stylers: [
          {
            color: '#ffffff',
          },
        ],
      },
      {
        featureType: 'landscape.man_made',
        elementType: 'geometry.fill',
        stylers: [
          {
            color: '#ffffff',
          },
        ],
      },
      {
        featureType: 'poi',
        elementType: 'all',
        stylers: [
          {
            visibility: 'on',
          },
        ],
      },
      {
        featureType: 'road',
        elementType: 'all',
        stylers: [
          {
            saturation: -100,
          },
          {
            lightness: 45,
          },
        ],
      },
      {
        featureType: 'road',
        elementType: 'geometry.fill',
        stylers: [
          {
            color: '#eeeeee',
          },
        ],
      },
      {
        featureType: 'road',
        elementType: 'labels.text.fill',
        stylers: [
          {
            color: '#7b7b7b',
          },
        ],
      },
      {
        featureType: 'road',
        elementType: 'labels.text.stroke',
        stylers: [
          {
            color: '#ffffff',
          },
        ],
      },
      {
        featureType: 'road.highway',
        elementType: 'labels.text.fill',
        stylers: [
          {
            visibility: 'off',
          },
        ],
      },
      {
        featureType: 'road.arterial',
        elementType: 'labels.text.fill',
        stylers: [
          {
            visibility: 'off',
          },
        ],
      },

      {
        featureType: 'water',
        elementType: 'all',
        stylers: [
          {
            color: '#6ab0f6',
          },
          {
            visibility: 'on',
          },
        ],
      },
      {
        featureType: 'water',
        elementType: 'geometry.fill',
        stylers: [
          {
            color: '#e1eaed',
          },
        ],
      },
      {
        featureType: 'water',
        elementType: 'labels.text.fill',
        stylers: [
          {
            color: '#070707',
          },
        ],
      },
      {
        featureType: 'water',
        elementType: 'labels.text.stroke',
        stylers: [
          {
            color: '#ffffff',
          },
        ],
      },

      {
        elementType: 'labels.icon',
        stylers: [
          {
            visibility: 'off',
          },
        ],
      },
      {
        featureType: 'poi',
        stylers: [
          {
            visibility: 'on',
          },
        ],
      },
      {
        featureType: 'road',
        elementType: 'labels.icon',
        stylers: [
          {
            visibility: 'on',
          },
        ],
      },
      {
        featureType: 'road.local',
        elementType: 'labels',
        stylers: [
          {
            visibility: 'off',
          },
        ],
      },
      {
        featureType: 'transit',
        stylers: [
          {
            visibility: 'off',
          },
        ],
      },
    ],
    zoom,
    zoomControl: false,
  };
  return conf;
}

function _geocode(address: string): Promise<{
  lat: number;
  lng: number;
}> {
  return new Promise((resolve, reject) => {
    const geocoder = new google.maps.Geocoder();
    const geocodeRequest = {
      address: address,
    };
    try {
      geocoder.geocode(geocodeRequest, (results: google.maps.GeocoderResult[] | null, status) => {
        if (status !== google.maps.GeocoderStatus.OK) {
          reject('Geocode was not successful for the following reason: ' + status);
          return;
        }

        if (isNil(results)) {
          reject('No result found');
          return;
        }

        const result = results[0];
        if (isNil(result)) {
          reject('No result found');
          return;
        }

        const location = result.geometry.location;
        resolve({
          lat: location.lat(),
          lng: location.lng(),
        });
      });
    } catch (error) {
      console.error('geocode', error);
    }
  });
}

function _get_unique_places(items: IPlaceList) {
  return Array.from(new Map(items.map((i) => [`${i.lng},${i.lat}`, i])).values());
}

function _is_able_to_render_directions(places: IPlaceList) {
  if (places.length <= 1) {
    return false;
  }

  // 所有的地址都是一模一样的
  if (_get_unique_places(places).length === 1) {
    return false;
  }

  const origin = find(places, { tag: LocationTag.Origin });
  const destination = find(places, { tag: LocationTag.Destination });

  return origin && destination;
}

function _is_meaningful_string(s: any) {
  return isString(s) && !isEmpty(s);
}

class GMapV2 {
  current_positions = [] as Array<MarkerWithLabel>;
  circles = [] as Array<google.maps.Circle>;
  markers = [] as Array<MarkerWithLabel>;

  directions_renderer: google.maps.DirectionsRenderer;
  directions_service: google.maps.DirectionsService;

  static create(google_map: google.maps.Map) {
    return new GMapV2(google_map);
  }

  constructor(private readonly google_map: google.maps.Map) {
    this.directions_renderer = (() => {
      const directions_renderer = new google.maps.DirectionsRenderer({
        map: google_map,
        suppressMarkers: true,
      });
      directions_renderer.setOptions({
        polylineOptions: {
          strokeColor: PolylineStrokeColor.B,
          strokeWeight: 4,
        },
      });
      return directions_renderer;
    })();
    this.directions_service = new google.maps.DirectionsService();
  }

  $reset() {
    if (this.directions_renderer) {
      this.directions_renderer.setMap(null);
    }

    this.clear_canvas();

    for (const i of this.current_positions) {
      if (i) {
        i.setMap(null);
      }
    }
    this.current_positions = [];
  }

  clear_canvas() {
    for (const i of this.circles) {
      if (i) {
        i.setMap(null);
      }
    }
    this.circles = [];

    for (const i of this.markers) {
      if (i) {
        i.setMap(null);
      }
    }
    this.markers = [];

    console.log('clear_canvas directions_service');

    this.directions_renderer.setMap(null);
    this.directions_renderer = (() => {
      const directions_renderer = new google.maps.DirectionsRenderer({
        map: this.google_map,
        suppressMarkers: true,
      });
      directions_renderer.setOptions({
        polylineOptions: {
          strokeColor: PolylineStrokeColor.B,
          strokeWeight: 4,
        },
      });
      return directions_renderer;
    })();
  }

  fit_bounds(places: IPlaceList) {
    if (places.length === 0) {
      return;
    }

    const google_map = this.google_map;
    if (places.length === 1) {
      const p = head(places);
      google_map.setCenter(
        isNil(p) ? DEFAULTS_CENTER_LOCATION : new google.maps.LatLng(p.lat, p.lng)
      );
      return;
    }

    // 所有的地址都是一模一样的
    if (_get_unique_places(places).length === 1) {
      const p = head(places);
      google_map.setCenter(
        isNil(p) ? DEFAULTS_CENTER_LOCATION : new google.maps.LatLng(p.lat, p.lng)
      );
      return;
    }

    const points = places;
    const lats = points.map((i) => i.lat).sort((a, b) => a - b);
    const lngs = points.map((i) => i.lng).sort((a, b) => a - b);
    if (lats.length === 0 || lngs.length === 0) {
      return;
    }

    const sw_lat = head(lats);
    const sw_lng = head(lngs);
    const ne_lat = last(lats);
    const ne_lng = last(lngs);

    if (isNil(sw_lat) || isNil(sw_lng) || isNil(ne_lat) || isNil(ne_lng)) {
      return;
    }

    const c = new google.maps.LatLng((ne_lat - sw_lat) / 2, (ne_lng - sw_lng) / 2);
    const sw = new google.maps.LatLng(sw_lat, sw_lng);
    const ne = new google.maps.LatLng(ne_lat, ne_lng);
    google_map.setCenter(c);
    google_map.fitBounds(new google.maps.LatLngBounds(sw, ne));
  }

  init(args: Partial<IProps>) {
    this.google_map.setOptions(_get_google_map_config(args));
  }

  set_options(args: Partial<IProps>) {
    this.google_map.setOptions(_get_google_map_config(args));
  }

  async get_valid_places(
    locations: ILocation[],
    opts: Partial<{
      getAddress: IGetAddress;
    }> = {}
  ): Promise<IPlaceList> {
    const {
      getAddress = (location) => {
        const address = _is_meaningful_string(location.zip)
          ? `${location.zip}`
          : _is_meaningful_string(location.city)
            ? `${location.city}`
            : _is_meaningful_string(location.street)
              ? `${location.street}`
              : '';
        return address;
      },
    } = opts;

    const valid_places: any = [];
    for (const location of locations) {
      if (location.lat && location.lng) {
        valid_places.push(
          (() => {
            const { lat, lng, ...others } = location;
            return {
              lat: isNumber(lat) ? lat : Number(lat),
              lng: isNumber(lng) ? lng : Number(lng),
              ...others,
            };
          })()
        );
        continue;
      }

      try {
        const addr = getAddress(location);
        if (addr.length > 0) {
          const coordination = await _geocode(addr);
          valid_places.push(assign({}, location, coordination));
        }
      } catch (error) {
        console.error('get_valid_locations', error);
      }
    }
    return valid_places;
  }

  async paint_directions(places: IPlaceList) {
    if (places.length === 0) {
      return;
    }

    console.log('paint_directions');

    try {
      const directions_renderer = this.directions_renderer;
      const directions_service = this.directions_service;

      const _render_directions = async (places: IPlaceList): Promise<void> => {
        return new Promise<void>((resolve) => {
          if (places.length < 2) {
            resolve();
            return;
          }

          const _request_route = (
            origin: IPlace,
            destination: IPlace,
            waypoints: google.maps.DirectionsWaypoint[] = []
          ) => {
            directions_service.route(
              {
                origin: new google.maps.LatLng(origin.lat, origin.lng),
                destination: new google.maps.LatLng(destination.lat, destination.lng),
                travelMode: google.maps.TravelMode.DRIVING,
                avoidFerries: true,
                waypoints,
                avoidTolls: true,
                optimizeWaypoints: true,
                durationInTraffic: true,
                avoidHighways: true,
              },

              (result: any, status: any) => {
                if (status !== google.maps.DirectionsStatus.OK) {
                  return;
                }

                directions_renderer.setDirections(result);
              }
            );
          };

          if (places.length === 2) {
            _request_route(head(places)!, last(places)!);
          } else {
            const origin = head(places)!;
            const destination = last(places)!;
            const waypoints: google.maps.DirectionsWaypoint[] = [];
            for (let index = 0; index < places.length; index++) {
              if (index === 0 || index === places.length - 1) {
                continue;
              }
              const i = places[index];
              waypoints.push({
                location: `${i.lat},${i.lng}`,
              });
            }
            _request_route(origin, destination, waypoints);
          }

          resolve();
        });
      };

      await _render_directions(places);
      this.fit_bounds(places);
    } catch (error: any) {
      console.error('safe_update_canvas', error);
    }
  }

  async paint_markers(
    places: IPlaceList,
    args: Partial<{
      marker_builder: IMarkerBuilder;
    }> = {}
  ) {
    const { marker_builder = build_marker } = args;
    if (places.length === 0) {
      return;
    }

    try {
      const google_map = this.google_map;
      const _render_markers = function _render_markers(
        places: IPlaceList,
        args: {
          marker_builder: IMarkerBuilder;
        }
      ) {
        const { marker_builder } = args;

        const markers = [] as MarkerWithLabel[];
        places
          .filter((i) => isNumber(i.lat) && isNumber(i.lng))
          .forEach((i) => {
            const customer_marker = marker_builder(i);
            if (!customer_marker) {
              return;
            }
            const marker = new MarkerWithLabel(
              assign(
                {},
                {
                  position: new google.maps.LatLng(i.lat, i.lng),
                  map: google_map,
                  labelAnchor: new google.maps.Point(0, 0),
                },
                customer_marker
              )
            );
            marker.addListener(
              'click',
              (me: MarkerWithLabel, event: google.maps.MapMouseEvent): void => {
                console.log('marker click', me, event);
                event_bus.emit(EVENT_MARKER_CLICK, {
                  event,
                  marker: me,
                });
              }
            );
            markers.push(marker);
          });
        return markers;
      };

      for (const i of this.markers) {
        if (i) {
          i.setMap(null);
        }
      }
      this.markers = [];
      this.markers = await _render_markers(places, {
        marker_builder,
      });
      this.fit_bounds(places);
    } catch (error) {
      console.error('safe_update_canvas', error);
    }
  }

  replace_marker(location: IPlace, args: { marker_builder: IMarkerBuilder }) {
    const { marker_builder } = args;

    const lookup = new Map(
      this.markers.map((i) => [`${i.getPosition()?.lng()},${i.getPosition()?.lat()}`, i])
    );
    const found = lookup.get(`${location.lng},${location.lat}`);
    if (!found) {
      return;
    }
    // remove marker from google map
    found.setMap(null);
    const marker = new MarkerWithLabel(
      assign(
        {},
        {
          position: new google.maps.LatLng(location.lat, location.lng),
          map: this.google_map,
          labelAnchor: new google.maps.Point(0, 0),
        },
        marker_builder(location)
      )
    );
    lookup.set(`${location.lng},${location.lat}`, marker);
    // remove found from markers array
    this.markers = Array.from(lookup.values());
  }

  async find_places(query: string) {
    return new Promise((resolve) => {
      const service = new google.maps.places.PlacesService(this.google_map);
      const request = {
        query,
        fields: ['formatted_address', 'name', 'geometry'],
      };

      service.findPlaceFromQuery(request, function (results, status) {
        console.log('service.findPlaceFromQuery', results, status);
        if (status === google.maps.places.PlacesServiceStatus.OK) {
          resolve(results);
          return;
        }
        resolve([]);
      });
    });
  }

  get_max_zoom_at(location: ILocation) {
    return new Promise<number>((resolve) => {
      const lat = Number(location.lat);
      const lng = Number(location.lng);
      if (!isNumber(lat) || !isNumber(lng)) {
        resolve(DEFAULT_ZOOM);
        return;
      }

      const srv = new google.maps.MaxZoomService();
      srv.getMaxZoomAtLatLng(new google.maps.LatLng(lat, lng), (r) => {
        if (r.status === google.maps.MaxZoomStatus.OK) {
          resolve(r.zoom as number);
          return;
        }

        resolve(DEFAULT_ZOOM);
      });
    });
  }

  async zoom_into(location: ILocation) {
    const lat = Number(location.lat);
    const lng = Number(location.lng);
    if (!isNumber(lat) || !isNumber(lng)) {
      return;
    }
    const google_map = this.google_map;
    const max_zoom = await this.get_max_zoom_at(location);
    google_map.setZoom(Math.min(16, max_zoom));
    google_map.setCenter(new google.maps.LatLng(lat, lng));
  }

  paint_circle(center: ILocation) {
    const lat = Number(center.lat);
    const lng = Number(center.lng);
    if (!isNumber(lat) || !isNumber(lng)) {
      return;
    }

    const circles = this.circles;
    const google_map = this.google_map;
    for (const i of circles) {
      if (i) {
        i.setMap(null);
      }
    }

    this.circles = (() => {
      const circles = [];
      const c = new google.maps.Circle({
        center: new google.maps.LatLng(lat, lng),
        clickable: false,
        fillColor: '#006ED0',
        fillOpacity: 0.1,
        map: google_map,
        radius: 3000,
        strokeColor: '#006ED0',
      });
      circles.push(c);
      return circles;
    })();
  }

  set_zoom(zoom: number) {
    const google_map = this.google_map;
    google_map.setZoom(Math.min(zoom, 21));
  }
}

type IGMap = ReturnType<typeof GMapV2.create>;

function useGMap(opts: { container: HTMLDivElement }) {
  return GMapV2.create(new google.maps.Map(opts.container, _get_google_map_config()));
}

export { GMapV2, useGMap };
export type { IGMap };
